#
# Copyright (c) 2021, 2023 supercell
#
# SPDX-License-Identifier: BSD-3-Clause
#

module Luce
  @@block_tags = [
    "blockquote",
    "h1",
    "h2",
    "h3",
    "h4",
    "h5",
    "h6",
    "hr",
    "li",
    "ol",
    "p",
    "pre",
    "ul",
    "address",
    "article",
    "aside",
    "details",
    "dd",
    "div",
    "dl",
    "dt",
    "figcaption",
    "figure",
    "footer",
    "header",
    "hgroup",
    "main",
    "nav",
    "section",
    "table",
    "thead",
    "tbody",
    "th",
    "tr",
    "td",
  ]

  protected def self.block_tags : Array(String)
    @@block_tags
  end

  # Translates a parsed AST to HTML.
  class HTMLRenderer < NodeVisitor
    property buffer : String::Builder
    property unique_ids : Set(String)

    @element_stack : Array(Element) = [] of Element
    @last_visited_tag : String?
    @tag_filter_enabled : Bool

    def initialize(enable_tag_filter : Bool = false)
      @buffer = String::Builder.new
      @unique_ids = Set(String).new
      @tag_filter_enabled = enable_tag_filter
    end

    def render(nodes : Array(Node)) : String
      nodes.each do |node|
        node.accept self
      end

      @buffer.to_s
    end

    def visit_text(text : Text) : Nil
      content = text.text_content

      content = filter_tags(content) if @tag_filter_enabled

      if ["br", "p", "li"].includes? @last_visited_tag
        lines = content.lines
        content = if content.includes? "<pre>"
                    lines.join("\n")
                  else
                    lines.map(&.lstrip).join("\n")
                  end
        if text.text_content.ends_with? "\n"
          content = "#{content}\n"
        end
      end
      @buffer << content

      @last_visited_tag = nil
    end

    def visit_element_before?(element : Element) : Bool
      # Hackish. Separate block-level elements with new lines.
      if @buffer.empty? == false && Luce.block_tags.includes? element.tag
        buffer.puts
      end

      buffer << "<#{element.tag}"

      element.attributes.each do |key, value|
        if v = value
          buffer << " #{key}=\"#{v}\""
        else
          buffer << " #{key}"
        end
      end

      generated_id = element.generated_id

      # attach header anchor ids generated from text
      unless generated_id.nil?
        buffer << " id=\"#{uniquify_id(generated_id)}\""
      end

      @last_visited_tag = element.tag

      if element.empty?
        # Empty element like <hr/>
        buffer << " />"

        buffer << "\n" if element.tag == "br"

        false
      else
        @element_stack << element
        buffer << ">"
        true
      end
    end

    def visit_element_after(element : Element) : Nil
      # ameba:disable Lint/NotNil
      if !element.children.nil? && !element.children.not_nil!.empty? &&
         Luce.block_tags.includes?(@last_visited_tag) &&
         Luce.block_tags.includes?(element.tag)
        buffer.puts
      elsif element.tag == "blockquote"
        buffer.puts
      end
      buffer << "</#{element.tag}>"

      @last_visited_tag = @element_stack.pop.tag
    end

    # Uniquifies an id generated from text.
    def uniquify_id(id : String) : String
      unless @unique_ids.includes? id
        @unique_ids.add(id)
        return id
      end

      suffix = 2
      suffixed_id = "#{id}-#{suffix}"
      while @unique_ids.includes? suffixed_id
        suffixed_id = "#{id}-#{(suffix += 1)}"
      end
      unique_ids.add(suffixed_id)
      suffixed_id
    end

    # Filter some particular tags, see:
    # https://github.github.com/gfm/#disallowed-raw-html-extension-
    #
    # NOTE: There is no dedicated syntax for this extension since, as per the
    # spec, this process should only happen when rendering the HTML output.
    private def filter_tags(content : String) : String
      content.gsub(
        Regex.new(
          "<(?=(?:" +
          "title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext" +
          ")>)", Regex::Options::IGNORE_CASE | Regex::Options::MULTILINE),
        "&lt;")
    end
  end
end
